跳到主要内容

Spring基础知识:IoC DI

参考资料

Spring 的官方文档 Spring IoC有什么好处呢? - Mingqi的回答 - 知乎

Spring 是一个开源的免费框架(容器) Spring 是一个 非入侵式的 框架(引入 Spring 不会对现有代码产生影响) 控制反转(IOC),面向切面编程(AOP)

配置环境

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>

什么是 IoC

实际上 IoC 就是设计模式中的依赖倒转原则

依赖倒转原则:

  • 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
  • 抽象不能依赖于具象,具象依赖于抽象。

什么是依赖倒置原则? 假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!


我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。

这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。

这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。

IoC 与 DI 的关系

控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:

为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:

这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:

由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:

由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:

这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:

这里只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。

不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写 Car类的单元测试,就只需要 Mock一下 Framework 类传入 Car就行了,而不用把 Framework, Bottom, Tire 全部 new一遍再来构造Car。

这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。

什么是控制反转容器

那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。

因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的 new。这里 IoC 容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,只需要维护一个 Configuration(可以是 xml 可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入 IoC Container 的第一个好处。

IoC Container的第二个好处是:在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车 instance 时候,是从底层往上层 new 的:

这个过程中,我们需要了解整个 Car/Framework/Bottom/Tire 类构造函数是怎么定义的,才能一步一步 new 注入。

而 IoC Container 在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步 new(有点像深度优先遍历):

这里 IoC Container 可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:

我们就像是工厂的客户。我们只需要向工厂请求一个 Car 实例,然后它就给我们按照 Config 创建了一个 Car 实例。我们完全不用管这个 Car 实例是怎么一步一步被创建出来。

简单实现一个例子

Inversion of Control(控制反转)是一种把创建对象的主动权交给使用者的思想(具体看下面这个案例),而依赖注入(Dependency Injection)只是其实现方式

// IoC 的例子

public class UserServiceImp implements UserService {
// 这里只使用接口来接受 “注入” 的实现类
private UserDao userDao;

/**
* 当用户使用这个 UserServiceImp 时利用 set 动态注入一个 userDao 的实现类,
* 而不是 new UserDaoImp()的形式在这个业务类里去创建一个 UserDaoImp 实现类
* 省的如果想要更换其他的 UserDaoImp 时还需要手动修改代码
*
* 而这种在使用时才传入一个实现类的方式叫做 “控制反转”
*/
@Override
public UserService setUserDao(UserDao userDao) {
this.userDao = userDao;
return this;
}

// 通过注入的 UserDaoImp 来取得 User 对象
@Override
public void getUser(){
userDao.getUser();
}
}

通过上面的例子可以看到系统的耦合性大大降低了,可以更加专注在业务的实现上,因为业务可以无需知道注入进来的是哪个实现类,只需使用这个接口去接收这个实现类就行了(里氏替换原则)

依赖注入:DI

Dependency Injection 即 “依赖注入”,其是 Spring 实现控制反转的方式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 使用spring来创建对象,在spring这些都称为Bean-->
<bean id="hello" class="com.alsritter.pojo.Hello">
<property name="str" value="Spring"/>
</bean>

</beans>

将实例对象委托给第三方,无需自己亲自去创建这个对象。一般搭配接口使用,使之可以不关心自动注入进来的实现类是哪个

public static void main(String[] args) {
//获取 Spring 的上下文对象
//这配置文件可以传入多个 public ClassPathXmlApplicationContext(String... configLocations)
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = (Hello) context.getBean("hello");
}

IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象

Set 方式注入

  • ref:引用容器中创建好的对象
  • value:传入的是基础数据类型

在 Java 中使用这个 Context 来获取交给容器管理的 Bean

<bean id="address" class="com.alsritter.pojo.Address"/>
<bean id="student" class="com.alsritter.pojo.Student">
<!-- 普通注入-->
<property name="name" value="测试名"/>
<!-- Bean 注入-->
<property name="address" ref="address"/>
<!-- 数组注入-->
<property name="books">
<array>
<value>红楼梦</value>
<value>西游记</value>
<value>水浒传</value>
<value>三国演义</value>
</array>
</property>
<!-- List注入-->
<property name="hobbies">
<list>
<value>听歌</value>
<value>敲代码</value>
<value>玩游戏</value>
</list>
</property>
<!-- Map注入-->
<property name="card">
<map>
<entry key="身份证" value="111111"/>
<entry key="银行卡" value="222222"/>
</map>
</property>
<!-- Set注入-->
<property name="games">
<set>
<value>女神异闻录</value>
<value>合金装备</value>
</set>
</property>
<!-- 传入一个null-->
<property name="wife">
<null/>
</property>
<!-- Properties配置文件,就像那个sql的那个一样-->
<property name="info">
<props>
<prop key="driver">111111</prop>
<prop key="url">url...</prop>
<prop key="username">root</prop>
<prop key="password">1234</prop>
</props>
</property>
</bean>

同理,添加一个实现类也无需动接口,直接在配置文件里添加对应的实现类 Bean 就好了

注意,这样创建的 Bean 是单例模式的

<bean id="mysqlImp" class="com.alsritter.dao.UserMysqlDaoImp"/>
<bean id="oracleImp" class="com.alsritter.dao.UserDaoOracleImp"/>
<bean id="defaultImp" class="com.alsritter.dao.UserDaoImp"/>


<bean id="userServiceImp" class="com.alsritter.service.UserServiceImp">
<property name="userDao" ref="defaultImp"/>
</bean>

构造器注入

<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="hight" value="20"/>
<constructor-arg name="width" value="42"/>
</bean>

用拓展工具注入

xmlns:p="http://www.springframework.org/schema/p"

xmlns:c="http://www.springframework.org/schema/c"

p 命名空间 注入,可以直接注入属性的值

<!-- 实际上就是property的缩写 -->

<!-- 原本 -->
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>

<!-- 使用了p命名空间 -->
<bean name="p-namespace" class="com.example.ExampleBean" p:email="someone@somewhere.com"/>

c 命名空间 注入

<!-- 实际上就是constructor的缩写 -->

<!-- 原本 -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>


<!-- 使用了c命名空间 -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

Bean 的作用域

ScopeDescription
singleton单例模式 (默认就是单例模式)
prototype原型模式 每次从容器中get的时候,都会产生一个新对象
request将单个 Bean 定义限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,
session将单个 Bean 定义限定为 HTTP 会话的生命周期。
application将单个Bean 定义限定为 ServletContext 的生命周期。
websocket将单个 Bean 定义限定为 WebSocket 的生命周期。

request、session、application、websocket 仅在支持web的Spring应用程序上下文中有效。

Example

<bean id="student2" class="com.alsritter.pojo.Student" p:name="张三" scope="prototype"/>

因为使用了原型模式,所以每取得一次就创建一个

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) context.getBean("student2");
Student student2 = (Student) context.getBean("student2");
System.out.println(student == student2);
}
-->
false

注解配置 Bean

需要先添加这个扫描标签

<!-- 指定要扫描的包,这个包下的注解就会生效 -->
<context:component-scan base-package="com.alsritter.pojo"/>

可以直接使用 @Component 注解,Spring 扫描到这个类后会自动将这个 Bean 添加到容器里

@Component
public class UserDaoImp implements UserDao {
@Override
public void getUser() {
System.out.println("获取了数据");
}
}

等价于

<bean id="userDaoImp" class="com.alsritter.dao.UserDaoImp"/>

因为有三层架构,所以也有单独的三个注解(但是效果都和 @Component 一样,都是注册到 Spring 容器里),这些不同的注解只是用来标识不同层

@Repository:dao层

@Service:service层

@Controller:controller层

如果要给字段赋值,直接在属性上(set 方法也行)加上 @Value 注解

@Value("小美")
String name;

等价于

<bean id="people" class="com.alsritter.pojo.People">
<property name="name" value="小美"/>
</bean>

配置作用域,效果同上

@Scope("singleton")

Spring 配置文件

Bean 别名

可以给 Bean 配置别名,且可以设置多个别名

<bean id="user" class="com.alsritter.pojo.User">
<property name="name" value="张三"/>
</bean>

<alias name="user" alias="user3"/>

Bean 标识

<!-- 
id:bean的唯一标识符,也就是相当于对象名
class:bean对象所对应的全限定名
name:也是别名,而且name可以取多个别名,可以用下例子的那些符号分隔
-->
<bean id="user2" class="com.alsritter.pojo.User" name="user4 user5;user6,user7">
<constructor-arg name="name" value="张三"/>
</bean>

导入其他的配置文件

Spring 可以创建多个配置文件再通过 import 整合起来

<import resource="beans.xml"/>
<import resource="beans2.xml"/>
<import resource="beans3.xml"/>

假设项目中有多个人协同开发,这三个人复制不同的类开发,不同的类需要注册在不同的 Bean 中,可以利用 import 将所有人的配置文件合并为一个总的,使用时直接用总的就可以了(一般叫 applicationContext.xml

注解的方式配置

就是使用注解来代替上面的 xml

配置文件

// 先在类上添加上这个Configuration注解,表名这个类是config
// 下面这个ComponentScan就是扫描包
// Import就是添加其他的配置类
@Configuration
@ComponentScan("com.alsritter.pojo")
@Import({MyConfig2.class,MyConfig3.class})
public class MyConfig {

// 注册一个Bean,就相当于我们之前写的一个Bean标签
// 这个方法的名字,就相当于bean标签中的id属性
// 这个方法的返回值就相当于bean标签中的class属性
// 注意,@Bean也是默认单例模式
@Bean
public User getUser(){
// 返回要注入到bean容器中的对象
return new User();
}
}

Bean 的配置

@Component
public class User {
@Value("小美")
private String name;
...

读取配置文件

public class MyTest {
public static void main(String[] args) {

/*
*如果完全使用了配置类方式去做,
* 我们就只能通过AnnotationConfigApplicationContext上下文来获取容器,
* 通过配置类的class对象加载
*/
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
//方法名就是Bean的名字
User getUser = (User) context.getBean("getUser");
System.out.println(getUser.getName());
}
}

自动装配 AutoWrite

Spring 会在上下文中自动寻找,并自动给 Bean 装配属性

有两种自动装配的方式,一种是通过 XML 文件的 byName 属性,另一种是使用注解 @Autowired

XML 的自动装配

XML的自动装配用法(只做了解,一般使用的是注解自动装配)

<!-- 以前手动引用对象的方式 -->
<bean id="cat" class="com.alsritter.pojo.Cat"/>
<bean id="dog" class="com.alsritter.pojo.Dog"/>

<bean id="people" class="com.alsritter.pojo.People">
<property name="name" value="小美"/>
<property name="cat" ref="cat"/>
<property name="dog" ref="dog"/>
</bean>

<!-- 使用自动装配 或使用byType-->
<bean id="people" class="com.alsritter.pojo.People" autowire="byName">
<property name="name" value="小美"/>
</bean>

id 和 name 的区别

自动装配的 byName 没有配置 name 时也可以使用 id,而 Spring 会给每个对象分配一个默认的 id

<bean id="/hello" class="com.alsritter.controller.HelloController"/>

配置文件中不允许出现两个 id 相同的,否则在初始化时即会报错

但配置文件中允许出现两个 name 相同的,在用 getBean() 返回实例时,后面一个 Bean 被返回,应该是前面那个被后面同名的 覆盖了。

为了避免不经意的同名覆盖的现象,尽量用 id 属性而不要用 name 属性。

注解的自动装配

要使用注解需要先添加支持

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<!-- 别忘了加上这句,开启注解的支持 -->
<context:annotation-config/>
<context:component-scan base-package="com.alsritter.pojo"/>

</beans>

直接在对象上面加上 @Autowired 注解

也可以加在 set 方法上

使用 @Autowired 之后可以不用再编写Set方法,前提是这个自动装配属性在 IoC 容器中存在,且符合名字 byName 因为框架采用的是反射,所以可以自动忽略访问修饰符

<bean id="cat" class="com.alsritter.pojo.Cat"/>
<bean id="dog" class="com.alsritter.pojo.Dog"/>
<bean id="people" class="com.alsritter.pojo.People"/>

使用注解自动装配

public class People {
@Autowired
private Cat cat;
...

注解指定注入对象

当名字不满足当前字段(或者不唯一),可以使用这个注解搭配 @Autowired 指定一个 Bean 给对象

注意:这个 @Autowired 默认注入方式是 byType

public class People {
@Autowired
@Qualifier("cat2222")
private Cat cat;
...

@Autowired 和 @Resource 的区别

补: 除了这个 @Autowired 自动注入注解,还有一个 效果一样 的注解 @Resource

@Resource 是java官方提供的注解,目的就是为了给这些容器框架一个标准,所以 Spring 也支持对这个注解进行注入,但是市面上容器框架不止一家,所以自己又做了个一样的注解 @Autowired 用来区分这个 @Resource

不过有一点,那就是 idea 不对 @Resource 进行检测(左边的标识)

@Autowired 和 @Resource 的区别

都是用来自动装配的,都可以放在属性字段上

@Autowired 通过 byType 的方式实现,而且必须要求这个对象存在!

@Resource 默认通过 byName 的方式实现,如果找不到名字,则通过 byType

注入 private 的原理

参考资料 spring通过注解方式依赖注入原理 (私有成员属性如何注入)

使用 Spring 的自动注入时会发现有时字段是使用 private 修饰的也能注入进来,而且没加 get set,也就没法通过反射拿到 get set 方法并注入它的依赖对象了,那它是怎么注入的呢?

如下代码:

@Service
public class Teacher {
@Resource
private Student student;

public void print(){
if(student!=null){
System.out.println("student name:"+student.getName());
}else{
System.out.println("student is null");
}
}
}

其实使用的还是反射,首先通过返回获取成员属性的注解,然后判断注解类型是根据对象类型还是名称注入,到这里都很好理解,关键在于私有对象如何注入,请看以下代码:

Field[] fields = Teacher.class.getDeclaredFields(); 
Student student = new Student();
student.setName("Alice");
Teacher teacher = new Teacher();
for (Field field : fields) {
if(field.getType().getName().equals(Student.class.getName())){
//关键点!设置私有成员属性为可访问!
field.setAccessible(true);
//将已创建的对象赋值
field.set(teacher, student);
}
}
teacher.print();

关键代码:

field.setAccessible(true);

使之可以直接读取私有字段